package org.springframework.security.web.authentication.session;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.util.Assert;

import java.util.Comparator;
import java.util.List;

/**
 * @author Dillon
 * @date 2024/7/14
 * @slogan 致敬大师 致敬未来的你
 * @desc session认证策略接口 控制session并发
 */
public class ConcurrentSessionControlAuthenticationStrategy
		implements MessageSourceAware, SessionAuthenticationStrategy {

	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	private final SessionRegistry sessionRegistry;

	private boolean exceptionIfMaximumExceeded = false;

	private int maximumSessions = 1;

	/**
	 * @param sessionRegistry the session registry which should be updated when the
	 *                        authenticated session is changed.
	 */
	public ConcurrentSessionControlAuthenticationStrategy(SessionRegistry sessionRegistry) {
		Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null");
		this.sessionRegistry = sessionRegistry;
	}

	/**
	 * 控制session并发
	 *
	 * @param authentication 认证成功对象
	 * @param request        请求对象
	 * @param response       响应对象
	 */
	@Override
	public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) {
		// 获取配置的session最大登录并发
		int allowedSessions = getMaximumSessionsForThisUser(authentication);
		// 如果为-1 则不做限制
		if (allowedSessions == -1) {
			return;
		}
		// 获取当前登录的所有的未过期的session列表
		List<SessionInformation> sessions = this.sessionRegistry.getAllSessions(authentication.getPrincipal(), false);
		int sessionCount = sessions.size();
		// 如果当前列表数量没有到最大限制数量 不处理
		if (sessionCount < allowedSessions) {
			return;
		}
		// 如果到了最大值 此时 加上当前session 为 max + 1
		if (sessionCount == allowedSessions) {
			// 获取当前登录session
			HttpSession session = request.getSession(false);
			if (session != null) {
				// 循环判断，如果当前session在列表中，则不处理，因为刚好到最大值
				for (SessionInformation si : sessions) {
					if (si.getSessionId().equals(session.getId())) {
						return;
					}
				}
			}
		}
		// 此时如果session不属于已存在session列表中的session 执行session过期策略
		allowableSessionsExceeded(sessions, allowedSessions, this.sessionRegistry);
	}

	/**
	 * 获取配置的允许最大登录并发数
	 *
	 * @param authentication 默认只允许一个session登录
	 * @return 如果 -1 则不做校验
	 */
	protected int getMaximumSessionsForThisUser(Authentication authentication) {
		return this.maximumSessions;
	}

	/**
	 * 执行session过期策略
	 *
	 * @param sessions          当前session
	 * @param allowableSessions 已存在的session列表
	 * @param registry          session注册器
	 * @throws SessionAuthenticationException session异常
	 */
	protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions,
			SessionRegistry registry) throws SessionAuthenticationException {
		// 看配置，如果不允许超过，抛异常
		if (this.exceptionIfMaximumExceeded || (sessions == null)) {
			throw new SessionAuthenticationException(
					this.messages.getMessage("ConcurrentSessionControlAuthenticationStrategy.exceededAllowed",
							new Object[]{allowableSessions}, "Maximum sessions of {0} for this principal exceeded"));
		}
		// 如果允许超过，则淘汰未活动最久的session
		sessions.sort(Comparator.comparing(SessionInformation::getLastRequest));
		int maximumSessionsExceededBy = sessions.size() - allowableSessions + 1;
		List<SessionInformation> sessionsToBeExpired = sessions.subList(0, maximumSessionsExceededBy);
		for (SessionInformation session : sessionsToBeExpired) {
			session.expireNow();
		}
	}

	/**
	 * Sets the <tt>exceptionIfMaximumExceeded</tt> property, which determines whether the
	 * user should be prevented from opening more sessions than allowed. If set to
	 * <tt>true</tt>, a <tt>SessionAuthenticationException</tt> will be raised which means
	 * the user authenticating will be prevented from authenticating. if set to
	 * <tt>false</tt>, the user that has already authenticated will be forcibly logged
	 * out.
	 *
	 * @param exceptionIfMaximumExceeded defaults to <tt>false</tt>.
	 */
	public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) {
		this.exceptionIfMaximumExceeded = exceptionIfMaximumExceeded;
	}

	/**
	 * Sets the <tt>maxSessions</tt> property. The default value is 1. Use -1 for
	 * unlimited sessions.
	 *
	 * @param maximumSessions the maximimum number of permitted sessions a user can have
	 *                        open simultaneously.
	 */
	public void setMaximumSessions(int maximumSessions) {
		Assert.isTrue(maximumSessions != 0,
				"MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum");
		this.maximumSessions = maximumSessions;
	}

	/**
	 * Sets the {@link MessageSource} used for reporting errors back to the user when the
	 * user has exceeded the maximum number of authentications.
	 */
	@Override
	public void setMessageSource(MessageSource messageSource) {
		Assert.notNull(messageSource, "messageSource cannot be null");
		this.messages = new MessageSourceAccessor(messageSource);
	}

}
